<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title>flux-challenge</title>
    <meta name="viewport" content="initial-scale=1">

    <!-- application styles -->
    <link rel="stylesheet" href="../../styles.css" type="text/css">

    <!-- 3rd party libraries: react.js, reflux, lodash, ... -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/superagent/1.2.0/superagent.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js"></script>
    <script src="https://cdn.jsdelivr.net/refluxjs/0.2.11/reflux.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.25/browser.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
</head>
<body>

<script type="text/babel">

'use strict';

// see src/constants.js

var DIRECTION = {
  UP: 'up',
  DOWN: 'down'
};

var SITH_LORD_LIST = {
  SIZE : 5,
  SCROLL_MOVE_POSITIONS: 2
};

// see src/utils.js

/* create list, full filled with 'empty' objects */
function emptyList(size = SITH_LORD_LIST.SIZE) {
  return _.fill(Array(size), { isEmptyObject: true });
};

// see src/empire.js

/** Jedi Master **/
class Jedi {
  constructor(name) {
    this.name = name;
  }
}

/** Sith Lord **/
class SithLord {

  constructor({name, id, homeworld, apprentice, master}) {
    this.id = id;
    this.name = name;
    this.homeworld = new Planet(homeworld);
    this.apprentice = apprentice;
    this.master = master;
  }

  isFromPlanet(planet) {
    return planet && this.homeworld.id === planet.id;
  }

  hasMaster() {
    return this.master && this.master.id;
  }

  hasApprentice() {
    return this.apprentice && this.apprentice.id;
  }

}

/** Planet in space, has a name and an unique identifier **/
class Planet {
  constructor({name: name, id: id}) {
    this.name = name;
    this.id = id;
  }
}

/** Spaceship - travels in space, lands on different planets **/
class Spacecraft {

  constructor(name, pilot) {
    this.name = name;
    this.pilot = pilot;
  }

  /*
    Ground Control to Major Tom
    Take your protein pills and put your helmet on
    Ground Control to Major Tom
    Commencing countdown, engines on
  */
  start(route) {
    if (this.ws) {
      this.ws.close();
    }
    this.ws = new WebSocket(route);
    this.ws.onmessage = (message => {
      var nextPlanet = new Planet(JSON.parse(message.data));
      Actions.planetLanding(nextPlanet);
    });
  }

  stop() {
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }
}

// see src/database.js

/** Request abstraction, has state (active/finished) and can be aborted **/
class Request {

  constructor(url) {
    this.url = url;
    this.isActive = false;
    this.isFinished = false;
  }

  start(callback) {
    this.process = superagent.get(this.url);
    this.isActive = true;
    this.process.end((err, res) => {
      this.isActive = false;
      this.isFinished = true;
      callback(res);
    });
  }

  abort() {
    if (this.isActive) {
      this.process.abort();
      this.isActive = false;
      this.isFinished = true;
    }
  }
}

/** Sith Lords database, only one active request in a time **/
class SithLordsDataBase {

  constructor() {
    this.requestsQueue = [];

    Store.listen(state => {
      // if UI is 'frozen' - stop all current request and clean up the queue
      if (state.isFrozen) {
        this.stopLoading();
        this.removeAll();
      } else if (!_.isEmpty(state.nextRequests)) {
        // if there are new Sith Lords to load - merge them with current:
        // 1. stop and remove all 'out of date' requests (no need them anymore in list)
        var expiredRequests = _.filter(this.requestsQueue, request => { return !_.contains(state.nextRequests, request.url); });
        this.stopLoading(expiredRequests);
        this.removeFinished();
        // 2. add 'new' requests in the queue, avoiding duplications
        var newRequests = _.filter(state.nextRequests, url => {
          return !_.find(this.requestsQueue, {url: url});
        });
        if (!_.isEmpty(newRequests)) {
          _.each(newRequests, this.load.bind(this));
        } else {
          this.startLoading();
        }
      }
    });
  }

  hasActiveRequests() {
    return _.any(this.requestsQueue, {isActive: true});
  }

  stopLoading(requests = this.requestsQueue) {
    _.each(requests, request => { request.abort(); });
  }

  removeFinished() {
    this.requestsQueue = _.filter(this.requestsQueue, {isFinished: false});
  }

  removeAll() {
    this.requestsQueue = [];
  }

  /* start loading all pending request one by one */
  startLoading() {
    if (!this.hasActiveRequests() && !_.isEmpty(this.requestsQueue)) {
      var request = _.first(this.requestsQueue);
      request.start(res => {
        this.removeFinished();
        this.startLoading();
        Actions.sithLordInformationLoading(new SithLord(res.body));
      });
    }
  }

  /* add a new request in a queue */
  load(url) {
    this.requestsQueue.push(new Request(url));
    this.startLoading();
  }
}


// see src/store.js

/** Sith Lord list.
 *  Wraps list of loaded Sith Lords and add placeholders for empty rows
 *  Immutable, any 'change' operation (add/scroll) produces a new list instance
 **/
class SithLordsList {

  constructor(sithLords = emptyList()) {
    this.sithLords = sithLords;
  }

  /* return only 'real' Sith Lords, no placeholders */
  getLoaded() {
    return _.reject(this.sithLords, {isEmptyObject: true});
  }

  /* 'true' if any of List Lords is from this 'planet' */
  hasSomeOneFrom(planet) {
    return _.any(this.getLoaded(), sithLord => {
      return sithLord.isFromPlanet(planet);
    });
  }

  /* first loaded Sith Lord from list of its index */
  getFirstLoaded() {
    var sithLords = this.getLoaded(),
      first = _.first(sithLords);
    return {
      sithLord: first,
      index: this.sithLords.indexOf(first)
    };
  }

  /*  last loaded Sith Lord from list of its index */
  getLastLoaded() {
    var sithLords = this.getLoaded(),
      last = _.last(sithLords);
    return {
      sithLord: last,
      index: this.sithLords.indexOf(last)
    };
  }

  /* add master's apprentice in list */
  addApprentice(apprentice, master) {
    var masterIndex = this.sithLords.indexOf(master),
      head = _.slice(this.sithLords, 0, masterIndex + 1),
      tail = _.slice(this.sithLords, masterIndex + 2);
    return new SithLordsList(head.concat([apprentice]).concat(tail));
  }

  /* add apprentice's master in list */
  addMaster(apprentice, master) {
    var apprenticeIndex = this.sithLords.indexOf(apprentice),
      head = _.slice(this.sithLords, 0, apprenticeIndex - 1),
      tail = _.slice(this.sithLords, apprenticeIndex);
    return new SithLordsList(head.concat([master]).concat(tail));
  }

  /* add new Sith Lord in list depending on its role - 'master'/'apprentice' */
  add(sithLord) {
    var master = _.find(this.sithLords, {apprentice: {id: sithLord.id}}),
      apprentice = _.find(this.sithLords, {master: {id: sithLord.id}});
    if (master) {
      return this.addApprentice(sithLord, master);
    } else if (apprentice) {
      return this.addMaster(apprentice, sithLord);
    } else {
      return new SithLordsList([sithLord].concat(_.slice(this.sithLords, 1)));
    }
  }

  /* moves rows in list 'up' */
  scrollDown() {
    var head = _.slice(this.sithLords, SITH_LORD_LIST.SCROLL_MOVE_POSITIONS),
      tail = emptyList(SITH_LORD_LIST.SCROLL_MOVE_POSITIONS);
    return new SithLordsList(head.concat(tail));
  }

  /* moves rows in list 'down' */
  scrollUp() {
    var head = emptyList(SITH_LORD_LIST.SCROLL_MOVE_POSITIONS),
      tail = _.slice(this.sithLords, 0, SITH_LORD_LIST.SIZE - SITH_LORD_LIST.SCROLL_MOVE_POSITIONS);
    return new SithLordsList(head.concat(tail));
  }

  /* moves rows in list 'down' or 'up' if it is possible, otherwise just creates a copy of current list*/
  scroll(direction) {
    if (_.isEmpty(this.getLoaded())) {
      return new SithLordsList(this.sithLords);
    }

    var first = this.getFirstLoaded(),
      last = this.getLastLoaded(),
      minAllowedIndex = SITH_LORD_LIST.SCROLL_MOVE_POSITIONS,
      maxAllowedIndex = SITH_LORD_LIST.SIZE - SITH_LORD_LIST.SCROLL_MOVE_POSITIONS - 1;

    if (direction === DIRECTION.UP && first.index <= maxAllowedIndex) {
      return this.scrollUp();
    } else if (direction === DIRECTION.DOWN && last.index >= minAllowedIndex){
      return this.scrollDown();
    } else {
      return new SithLordsList(this.sithLords);
    }
  }
}

/** Application 'actions' for components communication **/
var Actions = Reflux.createActions([
  'planetLanding',
  'sithLordInformationLoading',
  'scrollSithLordsList'
]);

/** Planet store. Contains planet, where spacecraft has landed **/
var PlanetStore = Reflux.createStore({

  state: {
    planet: null
  },

  init() {
    this.listenTo(Actions.planetLanding, this.onPlanetLanding);
  },

  onPlanetLanding(planet) {
    this.state = {planet: planet};
    this.trigger(this.state);
  }
});

/** Sith Lords list store. Contains Sith Lords list **/
var SithLordsListStore = Reflux.createStore({

  state: {
    list: new SithLordsList()
  },

  init() {
    this.listenTo(Actions.sithLordInformationLoading, this.onSithLordInformationLoading);
    this.listenTo(Actions.scrollSithLordsList, this.onScrollSithLordsList);
  },

  onSithLordInformationLoading(sithLord) {
    this.state = {list: this.state.list.add(sithLord)};
    this.trigger(this.state);
  },

  onScrollSithLordsList(direction) {
    this.state = {list: this.state.list.scroll(direction)};
    this.trigger(this.state);
  }
});

/** Main application store. Combines information about planet, Sith Lords list, pending Sith Lord loading requests, UI state **/
var Store = Reflux.createStore({

  state: {
    planet: PlanetStore.state.planet,
    list: SithLordsListStore.state.list,
    nextRequests: [],
    isFrozen: false
  },

  init() {
    this.listenTo(PlanetStore, this.newState);
    this.listenTo(SithLordsListStore, this.newState);
  },

  newState({planet = this.state.planet, list = this.state.list}) {
    var nextRequests = [],
      isFrozen = planet && list.hasSomeOneFrom(planet);

    // if any of loaded Sith Lords is from this planet - ' freeze' UI and no request
    if (!isFrozen) {
      // decide which Sith Lords load next
      var first = list.getFirstLoaded(),
        last = list.getLastLoaded();

      if (first.sithLord && first.sithLord.hasMaster() && first.index !== 0) {
        nextRequests.push(first.sithLord.master.url);
      }
      if (last.sithLord && last.sithLord.hasApprentice() && last.index !== list.sithLords.length - 1) {
        nextRequests.push(last.sithLord.apprentice.url);
      }
    }

    // application state
    this.state = {
      planet: planet,
      list: list,
      nextRequests: nextRequests,
      isFrozen: isFrozen
    };

    this.trigger(this.state);
  }

});

// see src/application.js


/** UI components **/

/** Spacecraft monitor - shows spacecraft pilot name and current planet when this spacecraft has landed **/
class SpacecraftMonitor extends React.Component {
  constructor({spacecraft}) {
    super({spacecraft});
    this.state = {
      pilot: spacecraft.pilot,
      planet: null
    };
  }

  componentDidMount() {
    this.unsubscribe = PlanetStore.listen(this.setState.bind(this));
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  render() {
    return (
      <h1 className="css-planet-monitor">
        {this.state.pilot.name} currently {this.state.planet ? 'on ' + this.state.planet.name : 'at home'}
      </h1>
    );
  }
}

/** Sith Lord list + scroll buttons **/
class SithLordList extends React.Component {

  constructor(options) {
    super(options);
    this.state = Store.state;
  }

  componentDidMount() {
    this.unsubscribe = Store.listen(this.setState.bind(this));
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  render() {
    var sithLords = _.map(this.state.list.sithLords, sithLord => {
      if (sithLord && sithLord.id) {
        var style = {
          color : sithLord.isFromPlanet(this.state.planet) ? 'red': ''
        };
        return (
          <li className="css-slot" style={style}>
            <h3>{sithLord.name}</h3>
            <h6>{sithLord.homeworld.name}</h6>
          </li>
        );
      } else {
        // empty row
        return (<li className="css-slot"></li>)
      }
    });

    return (
      <section className="css-scrollable-list">
        <ul className="css-slots">
          {sithLords}
        </ul>
        <div className="css-scroll-buttons">
          <ScrollButton direction={DIRECTION.UP}/>
          <ScrollButton direction={DIRECTION.DOWN}/>
        </div>
      </section>
    );
  }
}

/** Scroll button **/
class ScrollButton extends React.Component {
  constructor({direction}) {
    super({direction});
    this.state = Store.state;
  }

  componentDidMount() {
    this.unsubscribe = Store.listen(this.setState.bind(this));
  }

  componentWillUnmount() {
    this.unsubscribe();
  }

  render() {
    var noMoreItems,
      first = this.state.list.getFirstLoaded(),
      last = this.state.list.getLastLoaded(),
      disabled;

    if (this.props.direction === DIRECTION.UP) {
      noMoreItems = first.sithLord && !first.sithLord.hasMaster();
    } else {
      noMoreItems = last.sithLord && !last.sithLord.hasApprentice();
    }

    disabled = this.state.isFrozen || noMoreItems;

    return (
      <button onClick={() => {Actions.scrollSithLordsList(this.props.direction)}}
        disabled={disabled} className = { React.addons.classSet({
        'css-button-up': this.props.direction === DIRECTION.UP,
        'css-button-down': this.props.direction === DIRECTION.DOWN,
        'css-button-disabled': disabled
        })}>
      </button>
    );
  }
}


/** Create all application components and start application **/

var obiWan = new Jedi('Obi-Wan'),
  spacecraft = new Spacecraft('Delta-7', obiWan),
  sithLordsDataBase = new SithLordsDataBase();

// start 'spacecraft' and Sith Lords information loading
sithLordsDataBase.load('http://localhost:3000/dark-jedis/3616');
spacecraft.start('ws://localhost:4000');

// Application's UI
React.render(<SpacecraftMonitor spacecraft={spacecraft}/>,
  document.getElementById('spacecraft-monitor')
);

React.render(<SithLordList/>,
  document.getElementById('sithlords-list')
);

</script>

<div class="app-container">
    <div class="css-root">
        <div id="spacecraft-monitor"></div>
        <div id="sithlords-list"></div>
    </div>
</div>

</body>
</html>
